package com.thoughtworks.bytecode2class;

import com.thoughtworks.bytecode2class.attribute.Attribute;
import com.thoughtworks.bytecode2class.constantpool.ClassConstant;
import com.thoughtworks.bytecode2class.constantpool.ConstantPool;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ClassReader {

    private List<Byte> bytes = new LinkedList<Byte>();
    private int minorVersion;
    private int majorVersion;
    private int constantPoolCount;
    private int byteIndex = 0;
    private ConstantPool constantPool;
    private String magicNumber;
    private List<AccessFlag> accessFlags = new ArrayList<AccessFlag>();
    private int interfacesCount;
    private int fieldsCount;
    private int methodsCount;
    private List<MethodAndFieldRef> methods;
    private String thisClass;
    private String superClass;
    private int attributesCount;
    private List<Attribute> attributes;
    private List<String> interfaces;
    private List<MethodAndFieldRef> fields;

    public void read(InputStream input) throws IOException {
        buildByteArray(input);
        parseMagicNumber();
        parseMinorVersion();
        parseMajorVersion();
        parseConstantPoolCount();
        parseConstantPool();
        parseAccessFlag();
        parseThisClass();
        parseSuperClass();
        parseInterfacesCount();
        parseInterfaces();
        parseFieldsCount();
        parseFields();
        parseMethodsCount();
        parseMethods();
        parseAttributesCount();
        parseAttributes();
    }

    public void read(String classFile) throws IOException {
        read(new FileInputStream(classFile));
    }

    private void parseFields() {
        for (int i = 0; i < fieldsCount; i++) {
            fields.add(new MethodAndFieldRef(this));
        }
    }

    private void parseInterfaces() {
        for (int i = 0; i < interfacesCount; i++) {
            int nameIndex = forwardChar();
            interfaces.add(getConstantPool().getClassConstant(nameIndex).getName());
        }
    }

    private void parseAttributes() {
        for (int i = 0; i < attributesCount; i++) {
            attributes.add(Attribute.create(this));
        }
    }

    private void parseAttributesCount() {
        attributesCount = forwardChar();
        attributes = new ArrayList<Attribute>(attributesCount);
    }

    private void parseSuperClass() {
        int classIndex = forwardChar();
        ClassConstant constant = getConstantPool().getClassConstant(classIndex);
        superClass = constant.getName();
    }

    private void parseThisClass() {
        int classIndex = forwardChar();
        ClassConstant constant = getConstantPool().getClassConstant(classIndex);
        thisClass = constant.getName();
    }

    private void parseMethods() {
        for (int i = 0; i < methodsCount; i++) {
            methods.add(new MethodAndFieldRef(this));
        }
    }

    private void parseMethodsCount() {
        methodsCount = forwardChar();
        methods = new ArrayList<MethodAndFieldRef>(methodsCount);
    }

    private void parseFieldsCount() {
        fieldsCount = forwardChar();
        fields = new ArrayList<MethodAndFieldRef>(fieldsCount);
    }

    private void parseInterfacesCount() {
        interfacesCount = forwardChar();
        interfaces = new ArrayList<String>(interfacesCount);
    }

    private void parseMagicNumber() {
        if (forward() == 0xCA
                && forward() == 0xFE
                && forward() == 0xBA
                && forward() == 0xBE)
        magicNumber = "CAFEBABE";
    }

    public int forward() {
        return toDecimal(forwardByte());
    }

    private Byte forwardByte() {
        return bytes.get(byteIndex++);
    }

    private int toDecimal(byte b) {
        final int mask = 1 << 7;
        StringBuilder builder = new StringBuilder(8);
        for (int i = 0; i < 8; i++) {
            builder.append((mask & b) == mask ? "1" : "0");
            b = (byte) (b << 1);
        }
        return Integer.parseInt(builder.toString(), 2);
    }

    private void parseConstantPool() {
        while (constantPoolCount != constantPool.getCurrentCount()) {
            constantPool.create(this, forward());
        }
    }

    public List<Byte> forwardBytes(int count) {
        List<Byte> result = new ArrayList<Byte>(count);
        for (int i = 0; i < count; i++) {
            result.add(forwardByte());
        }
        return result;
    }

    private void parseConstantPoolCount() {
        constantPoolCount = forwardChar();
        constantPool = new ConstantPool();
    }
            
    private void parseMajorVersion() {
        majorVersion = forwardChar();
    }

    private void parseMinorVersion() {
        minorVersion = forwardChar();
    }

    private void parseAccessFlag() {
        int accessFlagInt = forwardChar();
        accessFlags = calculateAccessFlags(accessFlagInt);
    }

    public List<AccessFlag> calculateAccessFlags(int accessFlagInt) {
        List<AccessFlag> accessFlags = new ArrayList<AccessFlag>();
        for (AccessFlag accessFlag : AccessFlag.values()) {
            if ((accessFlag.getValue() | accessFlagInt) == accessFlagInt) {
                accessFlags.add(accessFlag);
            }
        }
        return accessFlags;
    }

//    private void buildByteArray(String classFile) throws IOException {
//        InputStream in = getClass().getResourceAsStream(classFile);
//        int i;
//        while ((i = in.read()) != -1) {
//            bytes.add((byte) i);
//        }
//    }

    private void buildByteArray(InputStream in) throws IOException {
        int i;
        while ((i = in.read()) != -1) {
            bytes.add((byte) i);
        }
    }

    public List<Byte> getBytes() {
        return bytes;
    }

    public String getMagicNumber() {
        return magicNumber;
    }

    public int getMinorVersion() {
        return minorVersion;
    }

    public int getMajorVersion() {
        return majorVersion;
    }

    public int getConstantPoolCount() {
        return constantPoolCount;
    }

    public ConstantPool getConstantPool() {
        return constantPool;
    }

    public List<AccessFlag> getAccessFlags() {
        return accessFlags;
    }

    public int getInterfacesCount() {
        return interfacesCount;
    }

    public int getFieldsCount() {
        return fieldsCount;
    }

    public int getMethodsCount() {
        return methodsCount;
    }

    public List<MethodAndFieldRef> getMethods() {
        return methods;
    }

    public String getThisClass() {
        return thisClass;
    }

    public String getSuperClass() {
        return superClass;
    }

    public int getAttributesCount() {
        return attributesCount;
    }

    public List<Attribute> getAttributes() {
        return attributes;
    }

    public List<String> getInterfaces() {
        return interfaces;
    }

    public List<MethodAndFieldRef> getFields() {
        return fields;
    }

    public char forwardChar() {
        return (char) (forward() * 256 + forward());
    }

    public int forwardWord() {
        return forwardChar() * 256 * 256 + forwardChar();
    }

    public long forwardDoubleWord() {
        return forwardWord() * 256L * 256L * 256L * 256L + forwardWord();
    }
}
